Skip to content

fix: Grid items overlap after returning from an extended background#593

Open
MatiPl01 wants to merge 2 commits into
mainfrom
fix/grid-overlap-on-resume
Open

fix: Grid items overlap after returning from an extended background#593
MatiPl01 wants to merge 2 commits into
mainfrom
fix/grid-overlap-on-resume

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Closes #592.

Sortable.Grid / Sortable.Flex item positions are driven entirely by Reanimated shared values and are never re-established when the app returns to the foreground. After an extended background, the platform can recreate the sortable's native views (and Reanimated may not re-run the mappers that position them), so the absolutely positioned items commit stale, overlapping positions and stay overlapped until a drag re-runs the layout.

On AppState becoming active this now:

  • drops the JS-side dimension cache so the onLayout events fired by recreated views are treated as fresh measurements instead of being diffed away, and
  • re-emits each inactive item's position from the authoritative layout (itemLayoutPositions) so the freshly committed views pick it up.

Shared between grid and flex; the currently dragged item is skipped, and the re-seed is a no-op while the grid is still in relative layout (nothing absolute to restore).

Before / after

Before (overlaps, only a drag fixes it) After (re-establishes on foreground)

The natural trigger is real-device memory reclaim over several minutes and cannot be scripted on an emulator (RN retains the surface across an Activity finish), so the recordings inject the exact resume-stale position state via a temporary debug hook to show the failure mode and the recovery, then background/foreground the app. In the "before" clip the overlap persists on return; in the "after" clip it snaps back to the correct layout with no drag.

Verification

Tested on an Android emulator (RN 0.86, Reanimated 4.5.1, Fabric). The failure mode was reproduced by injecting the resume-stale state that the "fixed by a drag" clue implies: the applied position.values go stale while itemHeights and itemLayoutPositions stay correct. Temporary logging on resume shows the fix restoring each item's position from the authoritative layout:

# after injecting the stale (overlapping) state, then background -> foreground:
posY before     = { "1": 0, "2": 6,  "3": 30 }    # applied positions stale (items overlapping)
after(=layoutY) = { "1": 0, "2": 70, "3": 335 }   # re-seeded from the (correct) computed layout

# on a normal resume (nothing stale) the same handler is a no-op:
posY before     = { "1": 0, "2": 70, "3": 335 }
after(=layoutY) = { "1": 0, "2": 70, "3": 335 }    # writes back the values already there

So the overlap recovers on the next foreground with no drag, and the handler does nothing (writes back identical values) when the layout is already correct. Expand/collapse, dynamic-height + Add reflow, and drag-reorder all keep working.

Also re-verified on the iOS fabric example (New Arch) as a direct A/B with the same injected resume-stale state:

  • Fix ON: injected overlap, then background + foreground, snaps back to the correct layout with no drag.
  • Fix OFF: injected overlap, then background + foreground, the overlap persists.

Note: this validates the fix against the failure mechanism (injected), not the un-forceable natural OS-reclaim trigger, which could not be reproduced on the available hardware (an emulator and a 12 GB device both keep the surface alive across an Activity finish). Reporter confirmation on the original device would close that last gap.

Follow-up commit

Refines the original change: gates the resume re-seed on usesAbsoluteLayout so it is skipped entirely while the grid is still in relative flow, and rewrites the explanatory comment to describe the real mechanism (a recreated native view never receives the last emitted style because the shared value did not change; re-assigning each inactive item's position as a fresh object forces Reanimated to re-push it, which is exactly what the first drag would otherwise do).

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
react-native-sortables-docs Ignored Ignored Preview Jul 8, 2026 9:00am

@MatiPl01 MatiPl01 self-assigned this Jul 7, 2026
@github-actions github-actions Bot added the fix label Jul 7, 2026
The AppState re-seed only matters once the grid uses absolute positioning; in relative flow there are no absolute positions to restore, so gate it on usesAbsoluteLayout and make it a no-op otherwise. Also correct the explanatory comment to describe the real mechanism: a recreated native view never receives the last emitted style because the shared value did not change, so re-assigning each inactive item's position (a fresh object) forces Reanimated to re-push the style, reproducing what the first drag would otherwise do.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Items overlap and layout breaks after returning from background (dynamic height with Reanimated)

1 participant